﻿// Copyright (c) 2012-2013 fancidev
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System;
using System.IO;
using System.Runtime.InteropServices;

namespace Util.IO
{
    public static class StreamExtensions
    {
        public static int ReadToEnd(this Stream stream, byte[] buffer, int offset, int count)
        {
            if (stream == null)
                throw new ArgumentNullException("stream");
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if (offset < 0 || offset > buffer.Length)
                throw new ArgumentOutOfRangeException("offset");
            if (count < 0 || count > buffer.Length - offset)
                throw new ArgumentOutOfRangeException("count");

            int n = 0;
            while (count > 0)
            {
                int actual = stream.Read(buffer, offset, count);
                if (actual < 0)
                    throw new ArgumentException("Bytes returned must be non-negative.");
                if (actual == 0)
                    break;
                n += actual;
                offset += actual;
                count -= actual;
            }
            return n;
        }

        public static bool TryReadFull(
            this Stream stream, byte[] buffer, int offset, int count)
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if (offset < 0)
                throw new ArgumentOutOfRangeException("offset");
            if (count < 0 || offset + count > buffer.Length)
                throw new ArgumentOutOfRangeException("count");

            while (count > 0)
            {
                int actual = stream.Read(buffer, offset, count);
                if (actual < 0)
                    throw new ArgumentException("Bytes returned must be non-negative.");
                if (actual == 0)
                    return false;
                offset += actual;
                count -= actual;
            }
            return true;
        }

        public static void ReadFull(
            this Stream stream, byte[] buffer, int offset, int count)
        {
            if (!TryReadFull(stream, buffer, offset, count))
                throw new EndOfStreamException();
        }

        public static void ReadFull(this Stream stream, byte[] buffer)
        {
            ReadFull(stream, buffer, 0, buffer.Length);
        }

        public static int ReadInt32(this Stream stream, Endian endian)
        {
            byte[] value = new byte[4];
            ReadFull(stream, value);

            bool isLittleEndian;
            switch (endian)
            {
                case Endian.Little:
                    isLittleEndian = true;
                    break;
                case Endian.Big:
                    isLittleEndian = false;
                    break;
                case Endian.Native:
                    isLittleEndian = BitConverter.IsLittleEndian;
                    break;
                default:
                    throw new ArgumentOutOfRangeException("endian");
            }

            int x = BitConverter.ToInt32(value, 0);
            if (isLittleEndian != BitConverter.IsLittleEndian)
                x = EndianConverter.SwapBytes(x);
            return x;
        }

        public static long ReadInt64(this Stream stream, Endian endian)
        {
            byte[] value = new byte[8];
            ReadFull(stream, value);

            if (endian == Endian.Native)
            {
                endian = BitConverter.IsLittleEndian ? Endian.Little : Endian.Big;
            }
            if (endian != Endian.Little && endian != Endian.Big)
            {
                throw new ArgumentOutOfRangeException("endian");
            }

            long x = 0;
            if (endian == Endian.Little)
            {
                for (int i = 0; i < 8; i++)
                {
                    x |= ((long)value[i] << (8 * i));
                }
            }
            else
            {
                for (int i = 0; i < 8; i++)
                {
                    x <<= 8;
                    x |= value[i];
                }
            }
            return x;
        }

        public static bool TryReadFull<TStruct>(
            this Stream stream, out TStruct var) where TStruct : struct
        {
            byte[] buffer = new byte[Marshal.SizeOf(typeof(TStruct))];
            if (!TryReadFull(stream, buffer, 0, buffer.Length))
            {
                var = new TStruct();
                return false;
            }

            GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
            var = (TStruct)Marshal.PtrToStructure(handle.AddrOfPinnedObject(),
                typeof(TStruct));
            handle.Free();
            return true;
        }

#if false
        public static void ReadFull(
            this System.IO.Stream stream, byte[] buffer, int offset, int count)
        {
            if (!TryReadFull(stream, buffer, offset, count))
                throw new System.IO.EndOfStreamException("Cannot read enough bytes.");
        }
#endif

        public static bool TryReadFull(this Stream stream, out int value)
        {
            byte[] buffer = new byte[4];
            if (TryReadFull(stream, buffer, 0, 4))
            {
                value = BitConverter.ToInt32(buffer, 0);
                return true;
            }
            else
            {
                value = 0;
                return false;
            }
        }

        public static bool TryReadByte(this Stream stream, out byte value)
        {
            int x = stream.ReadByte();
            if (x == -1)
            {
                value = 0;
                return false;
            }
            else
            {
                value = (byte)x;
                return true;
            }
        }

        /// <summary>
        /// Skips the given number of bytes in the stream, either by seeking
        /// if the stream supports seeking, or by reading and discarding data.
        /// </summary>
        /// <param name="count">Number of bytes to skip.</param>
        public static void Skip(this Stream stream, long count)
        {
            if (count < 0)
                throw new ArgumentOutOfRangeException("count", "count must be non-negative.");
            if (count == 0)
                return;

            if (stream.CanSeek)
            {
                stream.Seek(count, SeekOrigin.Current);
                return;
            }

            byte[] buffer = new byte[1024];
            while (count > 0)
            {
                int desired = (int)Math.Min(count, buffer.LongLength);
                int actual = stream.Read(buffer, 0, desired);
                if (actual == 0)
                    throw new EndOfStreamException();
                if (actual < 0 || actual > desired)
                    throw new ArgumentException("The underlying stream returned an incorrect byte count");
                desired -= actual;
            }
        }
    }
}
